﻿using ICodeShare.UI.Controls.Primitives;
using ICodeShare.UI.Controls.Utilities;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

namespace ICodeShare.UI.Controls
{
    [TemplatePart(Name = PART_TimeListItems, Type = typeof(ListBox))]
    public class TimePicker : DateTimePickerBase
    {
        private const string PART_TimeListItems = "PART_TimeListItems";

        #region Members

        private ListBox _timeListBox;
        private bool _isListBoxInvalid = true;
        internal static readonly TimeSpan EndTimeDefaultValue = new TimeSpan(23, 59, 0);
        internal static readonly TimeSpan StartTimeDefaultValue = new TimeSpan(0, 0, 0);
        internal static readonly TimeSpan TimeIntervalDefaultValue = new TimeSpan(1, 0, 0);

        #endregion //Members

        #region Properties

        #region EndTime

        public static readonly DependencyProperty EndTimeProperty = DependencyProperty.Register("EndTime", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(EndTimeDefaultValue, new PropertyChangedCallback(OnEndTimeChanged), new CoerceValueCallback(OnCoerceEndTime)));

        private static object OnCoerceEndTime(DependencyObject o, object value)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                return timePicker.OnCoerceEndTime((TimeSpan)value);
            else
                return value;
        }

        private static void OnEndTimeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                timePicker.OnEndTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
        }

        protected virtual TimeSpan OnCoerceEndTime(TimeSpan value)
        {
            ValidateTime(value);
            return value;
        }

        protected virtual void OnEndTimeChanged(TimeSpan oldValue, TimeSpan newValue)
        {
            InvalidateListBoxItems();
        }

        public TimeSpan EndTime
        {
            get
            {
                return (TimeSpan)GetValue(EndTimeProperty);
            }
            set
            {
                SetValue(EndTimeProperty, value);
            }
        }

        #endregion //EndTime

        #region Format

        protected override void OnFormatChanged(DateTimeFormat oldValue, DateTimeFormat newValue)
        {
            base.OnFormatChanged(oldValue, newValue);
            InvalidateListBoxItems();
        }

        #endregion //Format

        #region MaxDropDownHeight

        public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register("MaxDropDownHeight", typeof(double), typeof(TimePicker), new UIPropertyMetadata(130d, OnMaxDropDownHeightChanged));
        public double MaxDropDownHeight
        {
            get
            {
                return (double)GetValue(MaxDropDownHeightProperty);
            }
            set
            {
                SetValue(MaxDropDownHeightProperty, value);
            }
        }

        private static void OnMaxDropDownHeightChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                timePicker.OnMaxDropDownHeightChanged((double)e.OldValue, (double)e.NewValue);
        }

        protected virtual void OnMaxDropDownHeightChanged(double oldValue, double newValue)
        {
            // TODO: Add your property changed side-effects. Descendants can override as well.
        }

        #endregion

        #region StartTime

        public static readonly DependencyProperty StartTimeProperty = DependencyProperty.Register("StartTime", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(StartTimeDefaultValue, new PropertyChangedCallback(OnStartTimeChanged), new CoerceValueCallback(OnCoerceStartTime)));

        private static object OnCoerceStartTime(DependencyObject o, object value)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                return timePicker.OnCoerceStartTime((TimeSpan)value);
            else
                return value;
        }

        private static void OnStartTimeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                timePicker.OnStartTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
        }

        protected virtual TimeSpan OnCoerceStartTime(TimeSpan value)
        {
            ValidateTime(value);
            return value;
        }

        protected virtual void OnStartTimeChanged(TimeSpan oldValue, TimeSpan newValue)
        {
            InvalidateListBoxItems();
        }

        public TimeSpan StartTime
        {
            get
            {
                return (TimeSpan)GetValue(StartTimeProperty);
            }
            set
            {
                SetValue(StartTimeProperty, value);
            }
        }


        #endregion //StartTime

        #region TimeInterval

        public static readonly DependencyProperty TimeIntervalProperty = DependencyProperty.Register("TimeInterval", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(TimeIntervalDefaultValue, OnTimeIntervalChanged));
        public TimeSpan TimeInterval
        {
            get
            {
                return (TimeSpan)GetValue(TimeIntervalProperty);
            }
            set
            {
                SetValue(TimeIntervalProperty, value);
            }
        }

        private static object OnCoerceTimeInterval(DependencyObject o, object value)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                return timePicker.OnCoerceTimeInterval((TimeSpan)value);
            else
                return value;
        }

        private static void OnTimeIntervalChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = o as TimePicker;
            if (timePicker != null)
                timePicker.OnTimeIntervalChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
        }

        protected virtual TimeSpan OnCoerceTimeInterval(TimeSpan value)
        {
            ValidateTime(value);

            if (value.Ticks == 0L)
                throw new ArgumentException("TimeInterval must be greater than zero");

            return value;
        }


        protected virtual void OnTimeIntervalChanged(TimeSpan oldValue, TimeSpan newValue)
        {
            InvalidateListBoxItems();
        }

        #endregion //TimeInterval

        #endregion //Properties

        #region Constructors

        static TimePicker()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));
            FormatProperty.OverrideMetadata(typeof(TimePicker), new UIPropertyMetadata(DateTimeFormat.ShortTime));
        }

        #endregion //Constructors

        #region Base Class Overrides

        protected override void OnFormatStringChanged(string oldValue, string newValue)
        {
            if (this.Format == DateTimeFormat.Custom)
            {
                InvalidateListBoxItems();
            }
            base.OnFormatStringChanged(oldValue, newValue);
        }

        protected override void OnMaximumChanged(DateTime? oldValue, DateTime? newValue)
        {
            base.OnMaximumChanged(oldValue, newValue);
            this.InvalidateListBoxItems();
        }


        protected override void OnMinimumChanged(DateTime? oldValue, DateTime? newValue)
        {
            base.OnMinimumChanged(oldValue, newValue);
            this.InvalidateListBoxItems();
        }

        protected override void OnValueChanged(DateTime? oldValue, DateTime? newValue)
        {
            base.OnValueChanged(oldValue, newValue);

            // ListBox content may be affected if value's date changed and the date was
            // or is equal to Minimum or Maximum value.
            bool invalidate = false;

            if (DateTimeExtensions.IsSameDate(this.Minimum, oldValue)
              != DateTimeExtensions.IsSameDate(this.Minimum, newValue))
            {
                invalidate = true;
            }

            if (DateTimeExtensions.IsSameDate(this.Maximum, oldValue)
              != DateTimeExtensions.IsSameDate(this.Maximum, newValue))
            {
                invalidate = true;
            }

            // A value change can affect the display of the listbox items
            // if the Date part of the value has changed. This is the case when
            // the display text of the items contains part of the Date values.
            if (oldValue.GetValueOrDefault().Date != newValue.GetValueOrDefault().Date)
            {
                invalidate = true;
            }

            if (invalidate)
            {
                //Invalidate the entire listbox content
                this.InvalidateListBoxItems();
            }
            else
            {
                //Just update the selected item
                this.UpdateListBoxSelectedItem();
            }
        }

        protected override void Popup_Opened(object sender, EventArgs e)
        {
            base.Popup_Opened(sender, e);

            if (_timeListBox != null)
            {
                this.UpdateListBoxItems();

                TimeSpan time = (Value != null) ? Value.Value.TimeOfDay : StartTimeDefaultValue;
                TimeItem nearestItem = this.GetNearestTimeItem(time);
                if (nearestItem != null)
                {
                    _timeListBox.ScrollIntoView(nearestItem);
                    ListBoxItem listBoxItem = (ListBoxItem)_timeListBox.ItemContainerGenerator.ContainerFromItem(nearestItem);
                    if (listBoxItem != null)
                    {
                        listBoxItem.Focus();
                    }
                }
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (_timeListBox != null)
            {
                _timeListBox.SelectionChanged -= TimeListBox_SelectionChanged;
                _timeListBox.MouseUp -= TimeListBox_MouseUp;
            }

            _timeListBox = GetTemplateChild(PART_TimeListItems) as ListBox;

            if (_timeListBox != null)
            {
                _timeListBox.SelectionChanged += TimeListBox_SelectionChanged;
                _timeListBox.MouseUp += TimeListBox_MouseUp;

                InvalidateListBoxItems();
            }
        }

        #endregion //Base Class Overrides

        #region Event Handlers

        private void TimeListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count > 0)
            {
                TimeItem selectedTimeListItem = (TimeItem)e.AddedItems[0];
                var time = selectedTimeListItem.Time;

                if (this.UpdateValueOnEnterKey)
                {
                    var currentValue = this.ConvertTextToValue(this.TextBox.Text);
                    var date = currentValue ?? this.ContextNow;
                    var newValue = new DateTime(date.Year, date.Month, date.Day, time.Hours, time.Minutes, time.Seconds, time.Milliseconds, date.Kind);
                    this.TextBox.Text = newValue.ToString(this.GetFormatString(this.Format), this.CultureInfo);
                }
                else
                {
                    var date = this.Value ?? this.ContextNow;
                    this.Value = new DateTime(date.Year, date.Month, date.Day, time.Hours, time.Minutes, time.Seconds, time.Milliseconds, date.Kind);
                }
            }
        }

        private void TimeListBox_MouseUp(object sender, MouseButtonEventArgs e)
        {
            ClosePopup(true);
        }

        #endregion //Event Handlers

        #region Methods

        private void ValidateTime(TimeSpan time)
        {
            if (time.TotalHours >= 24d)
                throw new ArgumentException("Time value cannot be greater than or equal to 24 hours.");
        }

        public IEnumerable GenerateTimeListItemsSource()
        {
            TimeSpan time = StartTime;
            TimeSpan endTime = EndTime;

            if (endTime <= time)
            {
                endTime = EndTimeDefaultValue;
                time = StartTimeDefaultValue;
            }

            //Limit the content of the list to the Minimum or Maximum
            //if the date is set to the minimum or maximum.
            if (this.Value.HasValue)
            {
                DateTime date = this.Value.Value;
                DateTime minDate = this.Minimum.GetValueOrDefault(DateTime.MinValue);
                DateTime maxDate = this.Maximum.GetValueOrDefault(DateTime.MaxValue);
                TimeSpan minTime = minDate.TimeOfDay;
                TimeSpan maxTime = maxDate.TimeOfDay;

                if (date.Date == minDate.Date && time.Ticks < minTime.Ticks)
                {
                    time = minTime;
                }

                if (date.Date == maxDate.Date && endTime.Ticks > maxTime.Ticks)
                {
                    endTime = maxTime;
                }

                if (endTime < time)
                {
                    time = endTime;
                }
            }


            TimeSpan timeInterval = TimeInterval;
            List<TimeItem> timeItemList = new List<TimeItem>();

            if (time != null && endTime != null && timeInterval != null && timeInterval.Ticks > 0)
            {
                while (time <= endTime)
                {
                    timeItemList.Add(this.CreateTimeItem(time));
                    time = time.Add(timeInterval);
                }
            }
            return timeItemList;
        }

        protected virtual TimeItem CreateTimeItem(TimeSpan time)
        {
            var date = Value ?? this.ContextNow;
            string formatString = this.GetFormatString((DateTimeFormat)this.Format);
            return new TimeItem(date.Date.Add(time).ToString(formatString, CultureInfo), time);
        }

        private void UpdateListBoxSelectedItem()
        {
            if (_timeListBox != null)
            {
                TimeItem time = null;
                if (Value != null)
                {
                    time = this.CreateTimeItem(Value.Value.TimeOfDay);
                    if (!_timeListBox.Items.Contains(time))
                    {
                        time = null;
                    }
                }

                _timeListBox.SelectedItem = time;
            }
        }

        private void InvalidateListBoxItems()
        {
            _isListBoxInvalid = true;
            if (IsDropDownOpen)
            {
                UpdateListBoxItems();
            }
        }

        private void UpdateListBoxItems()
        {
            if (_timeListBox != null)
            {
                if (_isListBoxInvalid)
                {
                    _timeListBox.ItemsSource = GenerateTimeListItemsSource();
                    UpdateListBoxSelectedItem();
                    _isListBoxInvalid = false;
                }
            }
        }

        private TimeItem GetNearestTimeItem(TimeSpan time)
        {
            if (_timeListBox != null)
            {
                int itemCount = _timeListBox.Items.Count;
                for (int i = 0; i < itemCount; i++)
                {
                    TimeItem timeItem = _timeListBox.Items[i] as TimeItem;
                    if (timeItem != null)
                    {
                        if (timeItem.Time >= time)
                            return timeItem;
                    }
                }

                //They are all less than the searched time. 
                //Return the last one. (Should also be the greater one.)
                if (itemCount > 0)
                {
                    return _timeListBox.Items[itemCount - 1] as TimeItem;
                }
            }

            return null;
        }

        #endregion //Methods
    }
}